跳到主要内容

41 KMP子串查找算法

KMP子串查找算法(一)

  • 问题 如何在目标字符串S中,查找是否存在子串P?

  • 朴素解法

    int sub_str_index(const char *s, const char *p) {
    int ret = -1;
    int sl = strlen(s);
    int pl = strlen(p);
    int len = sl - pl;
    for (int i = 0; (ret < 0) && (i <= len); i++) {
    bool equal = true;
    for (int j = 0; equal && (j < pl); j++)
    equal = equal && (s[i + j] == p[j]);
    ret = (equal ? i : -1);
    }
    return ret;
    }
  • 朴素解法的一个优化线索

    因为,pa != pb,且pb == sb; 所以,pa != sb; 结论,子串p右移1位比较,没有意义。

  • 示例

  • 伟大的发现

    • 匹配失败时的右移位数与子串本身相关,与目标串无关
    • 移动位数 = 已匹配的字数-对应的部分匹配值
    • 任意子串都存在一个唯一的部分匹配表
  • 部分匹配表示例

    Partial Matched Table

    1234567
    ABCDABD
    0000120

    用法:

    第7位匹配失败→前6位匹配成功→查表PMT[6]→右移位数6-PMT[6]=6-2=4

  • 问题 部分匹配表是怎么得到的?

  • 前缀

    • 除了最后一个字符以外,一个字符串的全部头部组合
  • 后缀

    • 除了第一个字符以外,一个字符串的全部尾部组合
  • 部分匹配值

    • 前缀和后缀最长共有元素的长度
  • 示例:ABCDABD

-字符前缀后缀交集匹配值
1A0
2ABAB0
3ABCA,ABBC,C0
4ABCDA,AB,ABCBCD,CD,D0
5ABCDAA,AB,ABC,ABCDBCDA,CDA,DA,AA1
6ABCDABA,AB,ABC,ABCD,ABCDABCDAB,CDAB,DAB,AB,BAB2
7ABCDABDA,AB,ABC,ABCD,ABCDA,ABCDABBCDABD,CDABD,DABD,ABD,BD,D0
  • 问题 怎么编程产生部分匹配表?

  • 实现关键

    • PMT[1] = 0(下标为0的元素匹配值为0)
    • 从2个字符开始递推(从下标为1的字符开始递推)
    • 假设PMT[n]=PMT[n-1]+1(最长共有元素的长度)
    • 当假设不成立,PMT[n]在PMT[n-1]的基础上减小

编程实验(一)

  • 部分匹配表的递推与实现

    size_t* make_pmt(const char *p){
    auto len = strlen(p);
    auto *ret = reinterpret_cast<size_t*>(malloc(sizeof (size_t)*len));
    if(ret!=nullptr){
    size_t ll=0;
    ret[0] = 0;
    for (size_t i=1;i<len;i++) {
    while ((p[ll]!=p[i])&&(ll>0)){
    ll = ret[ll-1];
    }
    if(p[ll]==p[i]) ll++;
    ret[i]=ll;
    }
    }
    return ret;
    }

KMP子串查找算法(二)

  • 部分匹配表的使用(KMP算法)

    下标j处匹配失败→前j位匹配成功→查表PMT[j-1]→右移位数j-PMT[j-1]

    因为,s[i]!=p[j]

    ​ 所以,查表,LL=PMT[j-1]

    ​ 于是,右移,i的值不变,j的值改变,j=j-(j-LL)=LL=PMT[j-1];

编程实验(二)

  • KMP子串查找算法的实现

    int kmp(const char*s,const char*p){
    int ret = -1;
    auto sl = strlen(s);
    auto pl = strlen(p);
    auto pmt = make_pmt(p);
    if((pmt!=nullptr)&&(0<pl)&&(pl<=sl)){
    for (size_t i=0,j=0;i<sl;i++) {
    while ((j>0)&&(s[i]!=p[j])) {
    j=pmt[j-1];
    }
    if(s[i]==p[j]) j++;
    if(j==pl) ret = static_cast<int>(i+1-pl);
    }
    }
    free(pmt);
    return ret;
    }

小结

  • 部分匹配表是提高子串查找效率的关键
  • 部分匹配值定义为前缀和后缀最长共有元素的长度
  • 可以用递推的方法产生部分匹配表
  • KMP利用部分匹配表与子串移动位数的关系提高查找效率

42 KMP算法的应用

KMP算法的应用

  • 思考 如何在目标字符串中查找是否存在指定的子串?

    String s = "Hello World!";
    int pos = s.indexof("ll"); //2
  • 字符串类中的新功能

    成员函数功能描述
    indexOf(s)查找子串s在字符串中的位置
    remove(s)将字符串中的子串s删除
    operator-(s)定义字符串减法
    replace(s,t)将字符串中的子串s替换为t
    sub(i,len)从字符串中创建子串
  • 子串查找(KMP算法的直接应用)

    • int indexOf(const char *s)const
    • int indexOf(const String &s)const
  • 在字符串中将指定的子串删除

    • String& remove(const char *s)

    • String& remove(const String &s)

    1. 根据KMP在目标字符串中查找子串的位置

    2. 通过子串位置和子串长度进行删除

  • 字符串的减法操作定义(operator-)

    • 使用remove实现字符串间的减法操作
    • 字符串自身不被修改
    • 返回产生的新串
    String s1 = "abcde";
    String s2 = s1 - "bcd";
    cout << s1.str()<<endl; //abcde
    cout << s2.str()<<endl; //ae
  • 字符串中的子串替换

    • String& replace(const char *t,const char *s)
    • String& replace(const String &t,const char *s)
    • String& replace(const char *t,const String &s)
    • String& replace(const String &t,const String &s)
  • 从字符串中创建子串

    • String sub(int i,int len)const
    • 以i为起点提取长度为len的子串
    • 子串提取不会改变字符串本身的状态
    String s1 = "abcde";
    String s2 = s1.sub(1,3);
    cout << s1.str()<<endl; //abcde
    cout << s2.str()<<endl; //bcd

编程实验

  • 新成员函数的实现

    //KylinString.h
    #ifndef KYLINSTRING_H
    #define KYLINSTRING_H

    #include "Object.h"

    namespace KylinLib {

    class String
    {
    //...
    int indexOf(const char *s)const;
    int indexOf(const String &s)const;

    String& remove(size_t index,size_t length);
    String& remove(const char *s);
    String& remove(const String &s);

    String& replace(const char *t,const char *s);
    String& replace(const String &t,const char *s);
    String& replace(const char *t,const String &s);
    String& replace(const String &t,const String &s);

    String sub(size_t index,size_t length)const;

    String& operator-= (const char *str);
    String& operator-= (const String &str);

    String operator-(const char *str) const;
    String operator-(const String &str) const;

    protected:
    static size_t* make_pmt(const char *p);
    static int kmp(const char *s,const char *p);
    //...
    };
    }
    #endif // KYLINSTRING_H
    //KylinString.cpp
    #include "KylinString.h"
    #include "Exception.h"
    #include <stdlib.h>
    #include <string.h>

    namespace KylinLib {


    int String::indexOf(const char *s) const
    {
    return kmp(m_str,s);
    }

    int String::indexOf(const String &s) const
    {
    return indexOf(s.m_str);
    }

    String &String::remove(size_t index, size_t length)
    {
    if(index>=m_length)
    THROW_EXCEPTION(InvalidParameterException,"Index is invalid...");
    size_t begin = index+length;
    for (;begin<m_length;begin++,index++) {
    m_str[index]=m_str[begin];
    }
    m_str[index]='\0';
    m_length = index;
    return *this;
    }

    String &String::remove(const char *s)
    {
    auto pos = indexOf(s);
    if(pos>=0) remove(static_cast<size_t>(pos),strlen(s));
    return *this;
    }

    String &String::remove(const String &s)
    {
    return remove(s.m_str);
    }

    String &String::replace(const char *t, const char *s)
    {
    auto pos = indexOf(t);
    if(pos>0){
    remove(t);
    insert(static_cast<size_t>(pos),s);
    }
    return *this;
    }

    String &String::replace(const String &t, const char *s)
    {
    return replace(t.m_str,s);
    }

    String &String::replace(const char *t, const String &s)
    {
    return replace(t,s.m_str);
    }

    String &String::replace(const String &t, const String &s)
    {
    return replace(t.m_str,s.m_str);
    }

    String String::sub(size_t index, size_t length) const
    {
    if(index>=m_length)
    THROW_EXCEPTION(InvalidParameterException,"Parameter index is invalid...");
    if((index+length)>=m_length) length = m_length-index;

    auto str = reinterpret_cast<char*>(malloc(length+1));
    strcpy(str,m_str+index);
    str[length]='\0';
    String ret(str);
    free(str);
    return ret;
    }

    String &String::operator-=(const char *str)
    {
    return remove(str);
    }

    String &String::operator-=(const String &str)
    {
    return remove(str);
    }

    String String::operator-(const char *str)const
    {
    String ret(*this);
    return (ret-=str);
    }

    String String::operator-(const String &str)const
    {
    String ret(*this);
    return (ret-=str);
    }

    size_t *String::make_pmt(const char *p)
    {
    auto len = strlen(p);
    auto *ret = reinterpret_cast<size_t*>(malloc(sizeof (size_t)*len));
    if(ret!=nullptr){
    size_t ll=0;
    ret[0] = 0;
    for (size_t i=1;i<len;i++) {
    while ((p[ll]!=p[i])&&(ll>0)){
    ll = ret[ll-1];
    }
    if(p[ll]==p[i]) ll++;
    ret[i]=ll;
    }
    }
    return ret;
    }

    int String::kmp(const char *s, const char *p)
    {
    int ret = -1;
    auto sl = strlen(s);
    auto pl = strlen(p);
    auto pmt = make_pmt(p);
    if((pmt!=nullptr)&&(0<pl)&&(pl<=sl)){
    for (size_t i=0,j=0;i<sl;i++) {
    while ((j>0)&&(s[i]!=p[j])) {
    j=pmt[j-1];
    }
    if(s[i]==p[j]) j++;
    if(j==pl) ret = static_cast<int>(i+1-pl);
    }
    }
    free(pmt);
    return ret;
    }


    }

小结

  • 字符串类是工程开发中必不可少的组件
  • 字符串中应该包含常用字符串操作函数
  • 增:insert,operator+,...
  • 删:remove,operator-,...
  • 查:indexOf,...
  • 改:replace,...